import {
	BaseBoxShapeUtil,
	IndexKey,
	Polyline2d,
	ShapeUtil,
	TLAnyShapeUtilConstructor,
	TLHandle,
	TLHandleDragInfo,
	TLLineShape,
	TLShape,
	TLShapeId,
	Vec,
	VecModel,
	ZERO_INDEX_KEY,
} from '@tldraw/editor'
import { TestEditor } from './TestEditor'
import { TL } from './test-jsx'

const TEST1_TYPE = 'test1'

declare module '@tldraw/tlschema' {
	export interface TLGlobalShapePropsMap {
		[TEST1_TYPE]: { w: number; h: number; boundsSnapPoints: VecModel[] | null }
	}
}

type Test1Shape = TLShape<typeof TEST1_TYPE>

describe('custom shape bounds snapping - translate', () => {
	class TestShapeUtil extends BaseBoxShapeUtil<Test1Shape> {
		static override type = TEST1_TYPE
		override getDefaultProps() {
			return { w: 100, h: 100, boundsSnapPoints: null }
		}
		override component() {
			throw new Error('Method not implemented.')
		}
		override indicator() {
			throw new Error('Method not implemented.')
		}
		override getBoundsSnapGeometry(shape: Test1Shape) {
			return {
				points: shape.props.boundsSnapPoints ?? undefined,
			}
		}
	}
	const shapeUtils = [TestShapeUtil] as TLAnyShapeUtilConstructor[]

	let editor: TestEditor
	let ids: Record<string, TLShapeId>
	beforeEach(() => {
		editor = new TestEditor({ shapeUtils })
		ids = editor.createShapesFromJsx([
			<TL.geo ref="box" x={0} y={0} w={100} h={100} />,
			<TL.test1 ref="test1" x={200} y={200} w={100} h={100} boundsSnapPoints={null} />,
		])
	})

	describe('with default boundSnapPoints', () => {
		test('normal snapping works with default boundSnapPoints when moving test shape', () => {
			// start translating the test shape
			editor.setSelectedShapes([ids.test1]).pointerDown(250, 250)

			// move the left edge of the test shape to the right edge of the box shape - it should snap
			editor.pointerMove(155, 250, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(editor.getOnlySelectedShape()?.x).toBe(100)

			// move the left edge of the test shape to the center of the box shape - it should snap
			editor.pointerMove(105, 250, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(2)
			expect(editor.getOnlySelectedShape()?.x).toBe(50)
		})

		test('normal snapping works with default boundSnapPoints when snapping to test shape', () => {
			// start translating the box shape
			editor.setSelectedShapes([ids.box]).pointerDown(50, 50)

			// move the left edge of the box shape to the right edge of the test shape - it should snap
			editor.pointerMove(155, 50, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(editor.getOnlySelectedShape()?.x).toBe(100)

			// move the left edge of the box shape to the center of the test shape - it should snap
			editor.pointerMove(205, 50, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(2)
			expect(editor.getOnlySelectedShape()?.x).toBe(150)
		})
	})

	describe('with only the center in boundSnapPoints', () => {
		beforeEach(() => {
			editor.updateShape({
				id: ids.test1,
				type: TEST1_TYPE,
				props: { boundsSnapPoints: [{ x: 50, y: 50 }] },
			})
		})

		describe('when moving the test shape', () => {
			beforeEach(() => editor.select(ids.test1).pointerDown(250, 250))

			test('does not snap its edges to the box edges', () => {
				editor.pointerMove(155, 250, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(0)
				expect(editor.getOnlySelectedShape()?.x).toBe(105)
			})

			test('snaps its center to the box right edge', () => {
				editor.pointerMove(105, 250, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(1)
				expect(editor.getOnlySelectedShape()?.x).toBe(50)
			})
		})

		describe('when moving the box shape', () => {
			beforeEach(() => editor.select(ids.box).pointerDown(50, 50))

			test('does not snap to the left edge of the test shape', () => {
				editor.pointerMove(155, 50, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(0)
				expect(editor.getOnlySelectedShape()?.x).toBe(105)
			})

			test('snaps its right edge to the center of the test shape', () => {
				editor.pointerMove(205, 50, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(1)
				expect(editor.getOnlySelectedShape()?.x).toBe(150)
			})
		})
	})

	describe('with empty boundSnapPoints', () => {
		beforeEach(() => {
			editor.updateShape({
				id: ids.test1,
				type: TEST1_TYPE,
				props: { boundsSnapPoints: [] },
			})
		})

		test('test shape does not snap to anything', () => {
			editor.select(ids.test1).pointerDown(250, 250)

			// try to snap our left edge to the right edge of the box shape - it should not snap
			editor.pointerMove(155, 250, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(0)
			expect(editor.getOnlySelectedShape()?.x).toBe(105)

			// try to snap our left edge to the center of the box shape - it should not snap
			editor.pointerMove(105, 250, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(0)
			expect(editor.getOnlySelectedShape()?.x).toBe(55)
		})

		test('box shape does not snap to test shape', () => {
			editor.select(ids.box).pointerDown(50, 50)

			// try to snap our left edge to the right edge of the test shape - it should not snap
			editor.pointerMove(155, 50, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(0)
			expect(editor.getOnlySelectedShape()?.x).toBe(105)

			// try to snap our right edge to the center of the test shape - it should not snap
			editor.pointerMove(205, 50, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(0)
			expect(editor.getOnlySelectedShape()?.x).toBe(155)
		})
	})
})

const TEST2_TYPE = 'test2'

declare module '@tldraw/tlschema' {
	export interface TLGlobalShapePropsMap {
		[TEST2_TYPE]: {
			w: number
			h: number
			ownHandle: VecModel
			handleOutline: VecModel[] | 'default' | null
			handlePoints: VecModel[] | 'default'
			selfSnapOutline: VecModel[] | 'default'
			selfSnapPoints: VecModel[] | 'default'
			handleSnapType?: 'point' | 'align'
		}
	}
}

type Test2Shape = TLShape<typeof TEST2_TYPE>

describe('custom handle snapping', () => {
	class TestShapeUtil extends BaseBoxShapeUtil<Test2Shape> {
		static override type = TEST2_TYPE
		override getDefaultProps(): Test2Shape['props'] {
			return {
				w: 100,
				h: 100,
				ownHandle: { x: 0, y: 0 },
				handleOutline: 'default',
				handlePoints: 'default',
				selfSnapOutline: 'default',
				selfSnapPoints: 'default',
			}
		}
		override component() {
			throw new Error('Method not implemented.')
		}
		override indicator() {
			throw new Error('Method not implemented.')
		}
		override getHandleSnapGeometry(shape: Test2Shape) {
			const { handleOutline, handlePoints, selfSnapOutline, selfSnapPoints } = shape.props
			return {
				outline:
					handleOutline === 'default'
						? undefined
						: handleOutline === null
							? null
							: new Polyline2d({ points: handleOutline.map(Vec.From) }),
				points: handlePoints === 'default' ? undefined : handlePoints,

				getSelfSnapOutline:
					selfSnapOutline === 'default'
						? undefined
						: () => new Polyline2d({ points: selfSnapOutline.map(Vec.From) }),
				getSelfSnapPoints: selfSnapPoints === 'default' ? undefined : () => selfSnapPoints,
			}
		}
		override getHandles(shape: Test2Shape): TLHandle[] {
			const handle: TLHandle = {
				id: 'handle',
				label: 'handle',
				type: 'vertex',
				x: shape.props.ownHandle.x,
				y: shape.props.ownHandle.y,
				index: ZERO_INDEX_KEY,
			}

			if (shape.props.handleSnapType) {
				handle.snapType = shape.props.handleSnapType
			} else {
				// eslint-disable-next-line @typescript-eslint/no-deprecated
				handle.canSnap = true
			}

			return [handle]
		}
		override onHandleDrag(shape: Test2Shape, { handle }: TLHandleDragInfo<Test2Shape>) {
			return { ...shape, props: { ...shape.props, ownHandle: { x: handle.x, y: handle.y } } }
		}
	}
	const shapeUtils = [TestShapeUtil] as TLAnyShapeUtilConstructor[]

	let editor: TestEditor
	let ids: Record<string, TLShapeId>
	beforeEach(() => {
		editor = new TestEditor({ shapeUtils })
		ids = editor.createShapesFromJsx([
			<TL.line
				ref="line"
				x={0}
				y={0}
				points={{
					a1: { id: 'a1', index: 'a1' as IndexKey, x: 0, y: 0 },
					a2: { id: 'a2', index: 'a2' as IndexKey, x: 100, y: 100 },
				}}
			/>,
			<TL.test2 ref="test2" x={200} y={200} w={100} h={100} />,
		])
	})

	function startDraggingHandle() {
		const shape = editor.select(ids.line).getOnlySelectedShape()! as TLLineShape
		const handles = editor.getShapeHandles(shape)!
		editor.pointerDown(100, 100, { target: 'handle', shape, handle: handles[handles.length - 1] })
	}

	function handlePosition() {
		const shape = editor.select(ids.line).getOnlySelectedShape()! as TLLineShape
		const handles = editor.getShapeHandles(shape)!
		const handle = handles[handles.length - 1]
		return { x: handle.x, y: handle.y }
	}

	describe('with default handleSnapGeometry.outline', () => {
		test('snaps handles to the box of the shape', () => {
			startDraggingHandle()
			editor.pointerMove(215, 205, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(handlePosition()).toMatchObject({ x: 215, y: 200 })
		})

		test("doesn't particularly snap to vertices", () => {
			startDraggingHandle()
			editor.pointerMove(204, 205, undefined, { ctrlKey: true })
			// only snapped to the nearest edge, not the vertex
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(handlePosition()).toMatchObject({ x: 200, y: 205 })
		})

		test("doesn't snap to the center", () => {
			startDraggingHandle()
			editor.pointerMove(251, 251, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(0)
			expect(handlePosition()).toMatchObject({ x: 251, y: 251 })
		})
	})

	describe('with empty handleSnapGeometry.outline', () => {
		beforeEach(() => {
			editor.updateShape({
				id: ids.test2,
				type: TEST2_TYPE,
				props: { handleOutline: null },
			})
		})

		test("doesn't snap to the shape at all", () => {
			startDraggingHandle()
			editor.pointerMove(215, 205, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(0)
			expect(handlePosition()).toMatchObject({ x: 215, y: 205 })
		})
	})

	describe('with custom handleSnapGeometry.outline', () => {
		beforeEach(() => {
			editor.updateShape({
				id: ids.test2,
				type: TEST2_TYPE,
				props: {
					// a diagonal line from the top left to the bottom right
					handleOutline: [
						{ x: 0, y: 0 },
						{ x: 100, y: 100 },
					],
				},
			})
		})

		test('does not snap to the normal edges of the shape', () => {
			startDraggingHandle()
			editor.pointerMove(235, 205, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(0)
			expect(handlePosition()).toMatchObject({ x: 235, y: 205 })
		})

		test('snaps to the custom geometry', () => {
			startDraggingHandle()
			editor.pointerMove(210, 214, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(handlePosition()).toMatchObject({ x: 212, y: 212 })
		})
	})

	describe('with default handleSnapGeometry.points', () => {
		test('doesnt snap to the center', () => {
			startDraggingHandle()
			editor.pointerMove(251, 251, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(0)
			expect(handlePosition()).toMatchObject({ x: 251, y: 251 })
		})

		test('doesnt snap to corners', () => {
			startDraggingHandle()
			editor.pointerMove(203, 202, undefined, { ctrlKey: true })
			// snaps to edge, not corner:
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(handlePosition()).toMatchObject({ x: 203, y: 200 })
		})
	})

	describe('with custom handleSnapGeometry.points', () => {
		beforeEach(() => {
			editor.updateShape({
				id: ids.test2,
				type: TEST2_TYPE,
				props: {
					handlePoints: [
						{ x: 30, y: 30 },
						{ x: 70, y: 50 },
					],
				},
			})
		})

		test('snaps to the custom points', () => {
			startDraggingHandle()
			editor.pointerMove(235, 235, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(handlePosition()).toMatchObject({ x: 230, y: 230 })

			editor.snaps.clearIndicators()
			editor.pointerMove(265, 255, undefined, { ctrlKey: true })
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(handlePosition()).toMatchObject({ x: 270, y: 250 })
		})
	})

	describe('with custom handleSnapGeometry.points along the outline', () => {
		beforeEach(() => {
			editor.updateShape({
				id: ids.test2,
				type: TEST2_TYPE,
				props: {
					handlePoints: editor
						.getShapeGeometry(ids.test2)
						.bounds.cornersAndCenter.map(({ x, y }) => ({ x, y })),
				},
			})
		})

		test('snaps to points over outline', () => {
			startDraggingHandle()
			editor.pointerMove(203, 202, undefined, { ctrlKey: true })
			// snaps to corner, not edge:
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(handlePosition()).toMatchObject({ x: 200, y: 200 })
		})

		test('can still snap to non-outline points', () => {
			startDraggingHandle()
			editor.pointerMove(255, 255, undefined, { ctrlKey: true })
			// snaps to the center:
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(handlePosition()).toMatchObject({ x: 250, y: 250 })
		})

		test('can still snap to non-point outlines', () => {
			startDraggingHandle()
			editor.pointerMove(235, 205, undefined, { ctrlKey: true })
			// snaps to the edge:
			expect(editor.snaps.getIndicators()).toHaveLength(1)
			expect(handlePosition()).toMatchObject({ x: 235, y: 200 })
		})
	})

	describe('self snapping', () => {
		beforeEach(() => {
			editor.deleteShape(ids.line)
			editor.updateShape({
				id: ids.test2,
				type: TEST2_TYPE,
				x: 0,
				y: 0,
				props: {
					handlePoints: [{ x: 0, y: 0 }],
				},
			})
		})
		function startDraggingOwnHandle() {
			const shape = editor.select(ids.test2).getOnlySelectedShape()!
			const handles = editor.getShapeHandles(shape)!
			editor.pointerDown(0, 0, { target: 'handle', shape, handle: handles[0] })
		}
		function ownHandlePosition() {
			const shape = editor.select(ids.test2).getOnlySelectedShape()!
			const handle = editor.getShapeHandles(shape)![0]
			return { x: handle.x, y: handle.y }
		}
		describe('by default', () => {
			test('does not snap to standard outline', () => {
				startDraggingOwnHandle()
				editor.pointerMove(3, 50, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(0)
				expect(ownHandlePosition()).toMatchObject({ x: 3, y: 50 })
			})
			test('does not snap to standard points', () => {
				startDraggingOwnHandle()
				editor.pointerMove(3, 3, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(0)
				expect(ownHandlePosition()).toMatchObject({ x: 3, y: 3 })
			})
		})
		describe('with custom self snap outline & points', () => {
			beforeEach(() => {
				editor.updateShape({
					id: ids.test2,
					type: TEST2_TYPE,
					props: {
						selfSnapOutline: [
							{ x: 20, y: 50 },
							{ x: 80, y: 50 },
						],
						selfSnapPoints: [
							{ x: 20, y: 50 },
							{ x: 80, y: 50 },
						],
					},
				})
			})

			test('does not snap to standard outline', () => {
				startDraggingOwnHandle()
				editor.pointerMove(3, 50, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(0)
				expect(ownHandlePosition()).toMatchObject({ x: 3, y: 50 })
			})
			test('does not snap to standard points', () => {
				startDraggingOwnHandle()
				editor.pointerMove(3, 3, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(0)
				expect(ownHandlePosition()).toMatchObject({ x: 3, y: 3 })
			})
			test('snaps to the self-snap outline', () => {
				startDraggingOwnHandle()
				editor.pointerMove(50, 55, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(1)
				expect(ownHandlePosition()).toMatchObject({ x: 50, y: 50 })
			})
			test('snaps to the self-snap points', () => {
				startDraggingOwnHandle()
				editor.pointerMove(23, 55, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(1)
				expect(ownHandlePosition()).toMatchObject({ x: 20, y: 50 })
			})
		})

		describe('with snapType set to align', () => {
			beforeEach(() => {
				editor.updateShape({
					id: ids.test2,
					type: TEST2_TYPE,
					props: {
						selfSnapPoints: [
							{ x: 20, y: 50 },
							{ x: 60, y: 10 },
						],
						handleSnapType: 'align',
					},
				})
			})

			test('snaps to the y axis', () => {
				startDraggingOwnHandle()
				editor.pointerMove(18, 0, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(1)
				expect(ownHandlePosition()).toMatchObject({ x: 20, y: 0 })
			})

			test('snaps to the x axis', () => {
				startDraggingOwnHandle()
				editor.pointerMove(0, 48, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(1)
				expect(ownHandlePosition()).toMatchObject({ x: 0, y: 50 })
			})

			test('snaps to both axes', () => {
				startDraggingOwnHandle()
				editor.pointerMove(18, 9, undefined, { ctrlKey: true })
				expect(editor.snaps.getIndicators()).toHaveLength(2)
				expect(ownHandlePosition()).toMatchObject({ x: 20, y: 10 })
			})
		})
	})
})

const BEZIER_TYPE = 'bezier'

declare module '@tldraw/tlschema' {
	export interface TLGlobalShapePropsMap {
		[BEZIER_TYPE]: {
			start: VecModel
			cp1: VecModel
			cp2: VecModel
			end: VecModel
		}
	}
}

type BezierShape = TLShape<typeof BEZIER_TYPE>

describe('custom adjacent handle for shift snapping', () => {
	class BezierShapeUtil extends ShapeUtil<BezierShape> {
		static override type = BEZIER_TYPE
		override getDefaultProps() {
			return {
				start: { x: 0, y: 0 },
				cp1: { x: 50, y: 0 },
				cp2: { x: 50, y: 100 },
				end: { x: 100, y: 100 },
			}
		}
		override component() {
			throw new Error('Method not implemented.')
		}
		override indicator() {
			throw new Error('Method not implemented.')
		}
		override getGeometry() {
			return new Polyline2d({ points: [] })
		}

		override getHandles(shape: BezierShape): TLHandle[] {
			return [
				{
					id: 'start',
					type: 'vertex',
					index: 'a0' as IndexKey,
					x: shape.props.start.x,
					y: shape.props.start.y,
				},
				{
					id: 'cp1',
					type: 'vertex',
					index: 'a1' as IndexKey,
					x: shape.props.cp1.x,
					y: shape.props.cp1.y,
					snapReferenceHandleId: 'start', // cp1 snaps relative to start
				},
				{
					id: 'cp2',
					type: 'vertex',
					index: 'a2' as IndexKey,
					x: shape.props.cp2.x,
					y: shape.props.cp2.y,
					snapReferenceHandleId: 'end', // cp2 snaps relative to end
				},
				{
					id: 'end',
					type: 'vertex',
					index: 'a3' as IndexKey,
					x: shape.props.end.x,
					y: shape.props.end.y,
				},
			]
		}

		override onHandleDrag(shape: BezierShape, { handle }: TLHandleDragInfo<BezierShape>) {
			return {
				...shape,
				props: {
					...shape.props,
					[handle.id]: { x: handle.x, y: handle.y },
				},
			}
		}
	}

	const shapeUtils = [BezierShapeUtil] as TLAnyShapeUtilConstructor[]

	let editor: TestEditor
	let ids: Record<string, TLShapeId>

	beforeEach(() => {
		editor = new TestEditor({ shapeUtils })
		ids = editor.createShapesFromJsx([
			<TL.bezier
				ref="bezier"
				x={0}
				y={0}
				start={{ x: 0, y: 0 }}
				cp1={{ x: 50, y: 0 }}
				cp2={{ x: 50, y: 100 }}
				end={{ x: 100, y: 100 }}
			/>,
		])
	})

	test('cp1 snaps angle relative to start point when using custom adjacent handle', () => {
		editor.select(ids.bezier)
		const bezier = editor.getShape<BezierShape>(ids.bezier)!
		const cp1Handle = editor.getShapeHandles(bezier)!.find((h) => h.id === 'cp1')!

		// Start dragging cp1 handle
		editor.pointerDown(cp1Handle.x, cp1Handle.y, {
			target: 'handle',
			shape: bezier,
			handle: cp1Handle,
		})

		// Move with shift key - should snap angle relative to start (0, 0) not cp2
		editor.pointerMove(60, 20, { shiftKey: true })

		const updatedBezier = editor.getShape<BezierShape>(ids.bezier)!
		const cp1Pos = updatedBezier.props.cp1
		const startPos = updatedBezier.props.start

		// The angle from start to cp1 should be snapped to nearest 15 degrees
		const angle = Math.atan2(cp1Pos.y - startPos.y, cp1Pos.x - startPos.x)
		const degrees = (angle * 180) / Math.PI

		// Should snap to a multiple of 15 degrees (snapAngle uses 24 divisions = 15 degrees)
		const remainder = ((degrees % 15) + 15) % 15
		expect(Math.min(remainder, 15 - remainder)).toBeLessThan(1)
	})

	test('cp2 snaps angle relative to end point when using custom adjacent handle', () => {
		editor.select(ids.bezier)
		const bezier = editor.getShape<BezierShape>(ids.bezier)!
		const cp2Handle = editor.getShapeHandles(bezier)!.find((h) => h.id === 'cp2')!

		// Start dragging cp2 handle
		editor.pointerDown(cp2Handle.x, cp2Handle.y, {
			target: 'handle',
			shape: bezier,
			handle: cp2Handle,
		})

		// Move with shift key - should snap angle relative to end (100, 100)
		editor.pointerMove(80, 80, { shiftKey: true })

		const updatedBezier = editor.getShape<BezierShape>(ids.bezier)!
		const cp2Pos = updatedBezier.props.cp2
		const endPos = updatedBezier.props.end

		// The angle from end to cp2 should be snapped to nearest 15 degrees
		const angle = Math.atan2(cp2Pos.y - endPos.y, cp2Pos.x - endPos.x)
		const degrees = (angle * 180) / Math.PI

		// Should snap to a multiple of 15 degrees
		const remainder = ((degrees % 15) + 15) % 15
		expect(Math.min(remainder, 15 - remainder)).toBeLessThan(1)
	})

	test('default handles use default adjacent handle logic', () => {
		editor.select(ids.bezier)
		const bezier = editor.getShape<BezierShape>(ids.bezier)!
		const startHandle = editor.getShapeHandles(bezier)!.find((h) => h.id === 'start')!

		// Start dragging start handle
		editor.pointerDown(startHandle.x, startHandle.y, {
			target: 'handle',
			shape: bezier,
			handle: startHandle,
		})

		// Move with shift key - should use default logic (next vertex handle = cp1)
		editor.pointerMove(10, 10, { shiftKey: true })

		const updatedBezier = editor.getShape<BezierShape>(ids.bezier)!
		const startPos = updatedBezier.props.start
		const cp1Pos = updatedBezier.props.cp1

		// The angle from cp1 to start should be snapped to nearest 15 degrees
		const angle = Math.atan2(startPos.y - cp1Pos.y, startPos.x - cp1Pos.x)
		const degrees = (angle * 180) / Math.PI

		// Should snap to a multiple of 15 degrees
		const remainder = ((degrees % 15) + 15) % 15
		expect(Math.min(remainder, 15 - remainder)).toBeLessThan(1)
	})
})
